package heiing.collection.tree;

import heiing.collection.map.MapUtils;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 创建一棵树
 * @author heiing
 * @param <T> 树节点的类型
 * @param <K> 树节点的ID的类型
 */
public class Tree<T, K> {

    private final Map<K, T> map;
    private final Function<T, K> idprovider;
    private final Function<T, K> pidprovider;
    private final Function<T, String> nameprovider;

    /**
     * 创建树实例
     * @param items       树节点的集合
     * @param idMapper    将树节点映射到ID的方法
     * @param pidMapper   将树节点映射到其关联的父节点的ID的方法
     * @param nameMapper  将树节点映射到节点名称的方法
     */
    public Tree(Collection<T> items, Function<T, K> idMapper, Function<T, K> pidMapper, Function<T, String> nameMapper) {
        Objects.requireNonNull(idMapper, "Function id require non null");
        Objects.requireNonNull(pidMapper, "Function pid require non null");
        Objects.requireNonNull(nameMapper, "Function name require non null");
        this.idprovider = idMapper;
        this.pidprovider = pidMapper;
        this.nameprovider = nameMapper;
        this.map = MapUtils.toMap(items, idMapper, LinkedHashMap::new);
    }

    /**
     * 根据ID查询树节点
     * @param id 树节点的ID
     * @return 返回 id 对应的树节点，不存在则返回 null
     */
    public T get(K id) {
        return map.get(id);
    }

    /**
     * 根据ID查询树节点的父节点
     * @param id 树节点的ID
     * @return 返回 id 对应的树节点的父节点，不存在则返回 null
     */
    public T getParent(K id) {
        T self = get(id);
        return null == self ? null : get(pidprovider.apply(self));
    }

    /**
     * 根据ID查询树节点的所有子节点
     * @param id 树节点的ID
     * @return 返回 id 对应的树节点的所有子节点，不存在则返回空列表
     */
    public List<T> getChildren(K id) {
        return map.values().stream().filter(x -> Objects.equals(id, pidprovider.apply(x))).collect(Collectors.toList());
    }

    /**
     * 根据ID查询树节点的全限定名称，全限定名称是指从根节点到本节点的节点名称的拼接。
     * 父子之间的名称使用 / 分隔，例如 我公司/研发部/硬件研发组
     * @param id 树节点的ID
     * @return 返回 id 对应的树节点的全限定名称，不存在则返回 null
     */
    public String getNamePath(K id) {
        return getNamePath(id, "/");
    }

    /**
     * 根据ID查询树节点的全限定名称，全限定名称是指从根节点到本节点的节点名称的拼接。
     * 父子之间的名称使用 sep 分隔，假定 sep 为 | 则返回示例如：我公司|研发部|硬件研发组
     * @param id 树节点的ID
     * @param sep 连接父子之间的名称的分隔符
     * @return 返回 id 对应的树节点的以 sep 分隔的全限定名称，不存在则返回 null
     */
    public String getNamePath(K id, String sep) {
        T self = get(id);
        if (null == self) {
            return null;
        }
        LinkedList<String> paths = new LinkedList<>();
        paths.addFirst(nameprovider.apply(self));
        T p = getParent(id);
        while (null != p) {
            paths.addFirst(nameprovider.apply(p));
            p = getParent(idprovider.apply(p));
        }
        return String.join(sep, paths);
    }

    /**
     * 返回以节点ID为Key，节点为Value的字典
     * @return
     */
    public Map<K, T> getNodesMap() {
        return map;
    }

    /**
     * 将树节点映射到字典，例如 thisTree.toMap((k, t) -> t.getNamePath(k)) 表示建立以全限定名称为 Key 的字典
     * @param <M> 返回的字典的 Key 的类型
     * @param km 将树节点映射到 Key 的方法
     * @return 返回映射后的字典
     */
    public <M> Map<M, T> toMap(BiFunction<K, Tree<T, K>, M> km) {
        return map.keySet().stream().collect(LinkedHashMap::new, (m, k) -> m.put(km.apply(k, this), get(k)), LinkedHashMap::putAll);
    }

}
